Skip to content

test(scripts): pin the prerequisite exit code and advisory in the three i18n gates - #15181

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14857-i18n-prerequisite-pins
Sep 4, 2026
Merged

test(scripts): pin the prerequisite exit code and advisory in the three i18n gates#15181
baozhoutao merged 4 commits into
mainfrom
claude/issue-14857-i18n-prerequisite-pins

Conversation

@claude

@claude claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #14857

The three i18n gates answered PREREQUISITE NOT MET with an exit code and an advisory that names it, and pinned neither — while the four sibling gates that answer the same words pin both. Each printer built its string inline inside console.error(...) with process.exit(...) on the next line, so there was no value a test could read: the number PR #14856 moved from 1 to 3 was held in place here by nothing.

This extracts each refusal printer into a pure text function (the shape scripts/import-prerequisite.mjs already uses) and asserts, from each gate's own --self-test, the value that moved. No gate resisted — in all three the shape change stayed inside the printer and its call site, exactly the fence triage drew.

The success lines are updated in the same stroke: all three previously advertised only their classifiers, which is the reading triage kept — a pass line that overstates its own scope stops anyone looking.

Per gate

gate refusal paths exit code pinned advisory pinned resisted?
scripts/check-i18n-coverage.mjs 3 — PREREQUISITE NOT MET, COULD NOT MEASURE, POPULATION EMPTY yes, all 3 yes, all 3 no
scripts/check-i18n-bundles.mjs 1 printer, 5 call sites, both scanned renderings yes yes, both renderings no
packages/cli/scripts/check-app-nav-i18n.mjs 1 — the build prerequisite yes yes no

What each gate now asserts, per refusal path:

  • the prerequisite class is 3, and is distinct from EXIT_FINDINGS and from 0;
  • the refusal exits through the named constant, never a literal (pinned over the function body, not the constant — the regression that costs something is a process.exit(1) written back in, which leaves the constant reading 3 and every consumer green);
  • the printer prints the pinned text function — the seam without which an advisory pinned here is worthless;
  • the advisory interpolates the code rather than spelling one, names both numbers, carries no stale Exit code 1, and still states that nothing was measured.

Each gate also carries negative controls: every predicate is run against a function that does the forbidden thing and must see it, so a typo in either regex cannot pass forever.

COULD NOT MEASURE and POPULATION EMPTY sit behind the CLI-build probe — no local run and no other test observes them at all, so for those two these cases are the only observer. They were done first.

Rosters (#13799 b2 floors raised)

file batteries before → after roster-size floor new battery floor
check-i18n-coverage.mjs 4 → 5 4 → 5 23 (measured, not guessed)
check-i18n-bundles.mjs 4 → 5 4 → 5 14 (measured, not guessed)
check-app-nav-i18n.mjs no roster — deliberately not added (#13799's surface) 12 cases in the file's existing shape

Each new battery's floor was measured through the gate's own floor machinery (pin the battery at 999, read the count it names back), not counted by hand. Every new assertion registers through the existing registerCase() helper inside a declared battery; each file's verdict handshake is untouched (SELF_TEST_VERDICT sentinel in both rostered gates; the app-nav gate has none of the three spellings and did not grow one here).

Verification

All of it on 2745a1716, with every exit code captured by redirect before any pipe. Exit 3 is read as NOT MEASURED, never as a pass.

Self-testscheck-i18n-coverage ✓, check-i18n-bundles ✓, check-app-nav-i18n ✓ (exit 0 each).

Normal mode, live. On the unbuilt worktree all three refused with the real code: pnpm check:i18n-coverageexit 3, pnpm check:i18nexit 3, node packages/cli/scripts/check-app-nav-i18n.mjsexit 3, each printing Exit code 3, distinct from a finding's 1. That is 3 of the 5 paths observed live; the other two are the build-probe pair the pins now cover. The app-nav refusal is byte-identical before and after the extraction (diff of the two captured runs is empty) — the strongest available evidence that the extraction moved no text. After pnpm build (under the shared verify lock, 72/72 tasks) all three reach a real verdict: coverage OK (13 configs, 691 baselined), bundles OK (9 packages in sync), app-nav OK (10 contributors, 54 nav ids, 4 locales).

Ablation, 7 cases, one per pin class per gate. Each mutation is proven on disk by an exact substring count (anchor → 0, injected → n) and each restore by git hash-object equality against the HEAD blob plus an empty git diff HEAD — never by an editor's or a checkout's exit code; the harness restores under a trap and aborts loudly rather than letting a later measurement run on a mutated tree. These gates are plain .mjs scripts executed directly by node, with no dist/ between the edit and the run, so no rebuild leg applies.

  • exit code 3 → literal 1, in each of the three gates → self-test red, naming the refusal exits through the named constant, never a literal; restored → green.
  • advisory number corrupted to a spelled-out Exit code 1, in each of the three gates → self-test red, naming all three advisory pins (interpolates / names both numbers / no stale spelling); restored → green.
  • the seam: reportUnmeasuredConfigs stops printing unmeasuredConfigsText → self-test red, naming the printer prints the pinned text function; restored → green.

An earlier harness attempt is worth recording: its first run reported MUTATION ON DISK: anchor hits 1629 -> 1629 because grep -c -F counts LINES for a multi-line anchor, so its landing check was meaningless. The green self-test that followed is what caught it, and group A was re-run under exact-substring proof afterwards. Nothing was reported from the run that did not land.

Derived familynode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (derived from git at 2745a1716, 3 paths, three-dot semantics): 42 commands, all run, all green. pnpm check:dual-build-cjs-loads first returned exit 3 (NOT MEASURED, no build); after the build it measures and passes.

Always-runs tailcheck:pm-dispatch-gates ✓ (1343 cases), check:self-test-wired ✓, check-self-test-workflow-commands ✓ (no pnpm alias; run as node scripts/check-self-test-workflow-commands.mjs, plus its --self-test), check:declared-population-live ✓, check:ratchet-remedy-authority ✓, check:nul-bytes ✓. Plus app-nav-translation-parity.test.ts (the one sibling suite that names a touched gate, in comments) — 24/24.

Control bytesgrep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' over all three edited files: no match. Same scan over every body posted from this task.

skip-changeset: the diff publishes nothing. scripts/** ships in no package, and packages/cli/scripts/** is outside @objectstack/cli's files (dist, README.md, CHANGELOG.md).

Written by Claude Code in session session_012zGPuVVX3deAx9LdjK8jCk, dispatched by the domain:devx PM seat (claim comment 5535910063).


Generated by Claude Code

…rs into pure text functions

The three PREREQUISITE NOT MET / COULD NOT MEASURE / POPULATION EMPTY
printers built their string inline inside `console.error(...)`, so the exit
code each path returns and the advisory that names it had no value a test
could read. Same shape `scripts/import-prerequisite.mjs` already uses.

Text unchanged: only the wrapper and the trailing comma move.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
… own self-test

New battery, 23 cases, registered through the existing `registerCase()` helper:
the prerequisite class is 3, distinct from a finding's 1 and from 0; each of the
three printers exits through the named constant rather than a literal, prints
the pinned text function, interpolates the code rather than spelling one, and
renders an advisory that names both numbers with no stale `Exit code 1`.
Three negative controls prove each predicate can still fail.

Roster: 4 batteries -> 5 (SELF_TEST_BATTERY_FLOOR 4 -> 5); the new battery is
floored at its measured 23.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 4, 2026
@github-actions github-actions Bot added the size/m label Sep 4, 2026
… its own self-test

Extracts the refusal printer into `prerequisiteNotMetText` (message text
unchanged, both `scanned` branches move with it) and adds a 14-case battery:
the prerequisite class is 3, distinct from a finding's 1 and from 0; the printer
exits through the named constant, prints the pinned text function and
interpolates the code; both rendered advisories name both numbers with no stale
`Exit code 1`. Three negative controls prove each predicate can still fail.

Roster: 4 batteries -> 5 (SELF_TEST_BATTERY_FLOOR 4 -> 5); new battery floored
at its measured 14.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
…s own self-test

Extracts the refusal printer into `buildPrerequisiteText(probe)` (message text
unchanged — the live refusal on an unbuilt tree is byte-identical before and
after, still exit 3) and adds twelve cases in the file's existing self-test
shape: the prerequisite class is 3, distinct from a finding's 1 and from 0; the
refusal exits through the named constant, prints the pinned text function and
interpolates the code; the advisory names both numbers with no stale
`Exit code 1` and still names the probe file it was handed. Three negative
controls prove each predicate can still fail.

No battery roster added here — that is #13799's surface, not this card's.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 7bc5d37e428f9566ac8c4885d08d7f6b2bfe7789packageMentionDocs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants